// Mission Photo Application
// Copyright 2005-2006 (C) Ralph Thomas

#include <model/model.h>
#include <model/proxy/proxy.h>
#include <model/attributeNames.h>
#include <model/attributeInfo.h>
#include <view/factory.h>
#include <toolkit/application.h>
#include <util/resources.h>
#include <util/readFile.h>
#include <util/db.h>
#include <util/thread.h>
#include <datapumps/database.h>
#include <datapumps/substitute.h>
#include <datapumps/combine.h>
#include <datapumps/array_index.h>

#include <ui/function_adapter.h>
#include <ui/array_function_adapter.h>
#include <ui/array_datapump_adapter.h>
#include <apps/photo/photodb.h>
#include <apps/photo/import.h>
#include <apps/photo/flickr_pumps.h>
#include <apps/photo/gphoto_import.h>
#include <apps/photo/camera_monitor.h>

#include <boost/bind.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/fstream.hpp>

#include <client_assembler.hpp>
#include <factory.hpp>
#include <adobe/config.hpp>
#include <adobe/name.hpp>
#include <adobe/value.hpp>
#include <adobe/dictionary.hpp>
#include <adobe/array.hpp>
#include <adobe/adam.hpp>
#include <adobe/adam_evaluate.hpp>
#include <adobe/adam_parser.hpp>
#include <adobe/istream.hpp>
#include <adobe/xstring.hpp>
#include <adobe/future/file_slurp.hpp>

#include <fstream>
#include <sstream>
#include <iostream>
#include <iomanip>
#include <stdexcept>
#include <map>
// for getenv
#include <stdlib.h>

#ifdef ADOBE_PLATFORM_LINUX
#include <gtk/gtk.h>
#include <dbus/dbus-glib.h>
#include <pthread.h>
#endif

namespace bfs = boost::filesystem;

//
/// Return the path to the user's photo library. This is hardcoded
/// for now (eventually it would be nice to have a gconf/registry/defaults
/// abstraction).
///
/// \return	the path to the photo library.
//
bfs::path getPhotoLibraryPath() {
	//
	// XXX: using $HOME is probably a security risk if there are any
	// XXX: exploits in image decoders, etc.
	//
	bfs::path p( getenv( "HOME" ), bfs::native );
	return p / bfs::path( "Photo Library", bfs::native );
}
//
/// Add a new tag to the database.
///
/// \param	dict	the dictionary of parameters, including "db" and "tag".
//
void add_tag( const adobe::dictionary_t& dict ) {
	std::string tag( dict.get<std::string>( adobe::static_name_t( "tag" ) ) );
	apps::photo::add_tag( tag );
}
adobe::value_t array_function_factory( adobe::sheet_t& sheet, adobe::name_t name, const adobe::array_t& array ) {
	//
	// We have some predefined queries (just so that we don't have them in the .eve file and don't
	// have the scope to screw them up).
	//
	if( name == adobe::static_name_t( "tag_list" ) ) {
		adobe::dictionary_t dict;
		dict.set( adobe::static_name_t( "query" ), adobe::value_t( std::string( "select id, name from tags;" ) ) );
		dict.set( adobe::static_name_t( "db" ), adobe::value_t( std::string( "photo.db" ) ) );
		return datapumps::make_db_query_datapump( getPhotoLibraryPath(), dict, sheet );
	}
	if( name == adobe::static_name_t( "substitute" ) ) {
		std::string dest;
		adobe::dictionary_t source;
		if( array.size() != 2 )
			throw std::runtime_error( "substitute function takes two arguments" );
		if( !array.get( 0, dest ) )
			throw std::runtime_error( "substitute function's first argument is a string" );
		if( !array.get( 1, source ) )
			throw std::runtime_error( "substitute function's second argument is a dictionary" );
		return datapumps::make_substitute_datapump( dest, source, sheet );
	}
	if( name == adobe::static_name_t( "db_escape" ) )
		return adobe::value_t( app::array_datapump_adapter_t<std::string(std::string)>::create( boost::function<std::string(std::string)>( &apps::photo::escape_db ), sheet, array ) );
	if( name == adobe::static_name_t( "combine" ) )
		return datapumps::make_combine_datapump( array, sheet );
	if( name == adobe::static_name_t( "flickr_state" ) )
		return flickr::make_flickr_state();
	if( name == adobe::static_name_t( "flickr_connect" ) )
		return flickr::make_flickr_connect();
	if( name == adobe::static_name_t( "flickr_auth" ) )
		return flickr::make_flickr_auth();
	if( name == adobe::static_name_t( "gphoto_state" ) )
		return apps::photo::make_gphoto_state();
	if( name == adobe::static_name_t( "gphoto_camera_list" ) )
		return apps::photo::make_gphoto_camera_list();
	if( name == adobe::static_name_t( "gphoto_selected_camera" ) )
		return apps::photo::make_gphoto_selected_camera();
	if( name == adobe::static_name_t( "gphoto_progress" ) )
		return apps::photo::make_gphoto_progress();
	if( name == adobe::static_name_t( "gphoto_progress_url" ) )
		return apps::photo::make_gphoto_progress_url();
	if( name == adobe::static_name_t( "gphoto_rescan_cameras" ) )
		return apps::photo::make_gphoto_rescan();
	
	throw std::runtime_error( "Unknown function" );
}
//
/// Function factory, returns all of the custom datapumps. We
/// don't have the open_url action available in the photo app
/// because everything happens in one window.
//
adobe::value_t dict_function_factory( adobe::sheet_t& sheet, adobe::name_t name, const adobe::dictionary_t& dict ) {
	if( name == adobe::static_name_t( "db_query" ) )
		return datapumps::make_db_query_datapump( getPhotoLibraryPath(), dict, sheet );
	if( name == adobe::static_name_t( "db_row" ) )
		return datapumps::make_db_row_datapump( getPhotoLibraryPath(), dict, sheet );
	if( name == adobe::static_name_t( "db_value" ) )
		return datapumps::make_db_value_datapump( getPhotoLibraryPath(), dict, sheet );
	if( name == adobe::static_name_t( "add_tag" ) )
		return adobe::value_t( app::function_adapter_t::create( &add_tag, sheet, dict ) );
	if( name == adobe::static_name_t( "combine" ) )
		return datapumps::make_combine_datapump( dict, sheet );
	if( name == adobe::static_name_t( "flickr_upload" ) )
		return flickr::make_flickr_upload( dict, sheet );
	if( name == adobe::static_name_t( "gphoto_import" ) )
		return apps::photo::make_gphoto_import( dict, sheet, getPhotoLibraryPath() );
	if( name == adobe::static_name_t( "array_index" ) )
		return datapumps::make_array_index_datapump( dict, sheet );
	throw std::runtime_error( "Unknown function" );
}
//
/// Updates the given sheet with the given progress. The progress
/// is put into the @import_progress cell, the current filename is
/// put into the @import_file cell.
///
/// \param	sheet		the sheet to update.
/// \param	file		the current file.
/// \param	progress	the current progress (percentage).
//
void update_sheet_progress( adobe::sheet_t& sheet, const std::string& file, double progress ) {
	sheet.set( adobe::static_name_t( "import_file" ), adobe::value_t( file ) );
	sheet.set( adobe::static_name_t( "import_progress" ), adobe::value_t( progress ) );
	sheet.update();
}
//
/// This is the function that should be called from another thread
/// when adding photos. It is very similar to the above, but ensures
/// that the above is run in the main thread.
//
void thread_update_sheet_progress( adobe::sheet_t& sheet, const std::string& file, double progress ) {
	toolkit::application::getInstance().executeInMain( boost::bind( &update_sheet_progress, boost::ref( sheet ), file, progress ) );
}
//
/// This function can be executed from another thread, and imports
/// all of the photos in the given directory.
///
/// \param	sheet	the sheet to update with progress info.
/// \param	dir	the directory to import from.
/// \param	tag	the tag to apply.
/// \param	end	function to call when done.
//
class import_directory_work_t : public util::thread::work {
	bfs::path			source_dir_m;
	std::string			tag_m;
	apps::photo::progress_func_t	progress_m;
	toolkit::thread_function_t	end_m;
  public:
	import_directory_work_t( const bfs::path& dir, const std::string& tag, const apps::photo::progress_func_t& progress, const toolkit::thread_function_t& end )
	: source_dir_m( dir ), tag_m( tag ), progress_m( progress ), end_m( end ) {}

	virtual void doWork() {
		try {
			apps::photo::import_directory( source_dir_m, apps::photo::getPhotoLibraryPath(), apps::photo::kCopy, tag_m, progress_m );
		} catch( std::exception& ex ) {
			std::cout << "import failed: " << ex.what() << std::endl;
		}
		if( end_m ) {
			toolkit::application::getInstance().executeInMain( end_m );
		}
	}
	virtual priority getPriority() const { return kWorkPriorityRegular; }
};
//
//
/// This controller class loads up a sheet and the GUI. The rest should be
/// handled by ASL.
//
class controller {
	adobe::sheet_t& _sheet;
	eve_client::window_server_t _ws;
	view::factory _factory;
	apps::photo::camera_monitor_t::camera_monitor_ptr_t _camera_monitor;
	apps::photo::camera_monitor_t::scoped_connection_t _camera_connection;
	//
	// Note: it's okay to keep lots of these around, the thread gets
	//       created only when needed (and should get destroyed when
	//       it's done).
	//
	static util::thread _importThread;
	static int _refCount;
	//
	/// This is the main array_function_factory. It implements the "open_window"
	/// function and then defers to the other dict_function_factory for everything
	/// else.
	//
	adobe::value_t array_function_factory( adobe::sheet_t& sheet, adobe::name_t name, const adobe::array_t& array ) {
		if( name == adobe::static_name_t( "open_window" ) )
			return adobe::value_t( app::array_function_adapter_t<void (std::string)>::create( boost::bind( &controller::open_window_eve, this, _1 ), sheet, array ) );
		if( name == adobe::static_name_t( "close_window" ) )
			return adobe::value_t( app::array_function_adapter_t<void ()>::create( boost::bind( &controller::close_window, this ), sheet, array ) );
		return ::array_function_factory( sheet, name, array );
	}
	//
	/// This is the main dictionary function factory. It implements the
	/// "import_directory" function.
	//
	adobe::value_t dictionary_function_factory( adobe::sheet_t& sheet, adobe::name_t name, const adobe::dictionary_t& dict ) {
		if( name == adobe::static_name_t( "import_directory" ) ) {
			adobe::binder_ptr_t binder = app::function_adapter_t::create( boost::bind( &controller::import_directory_dict, this, _1 ), sheet, dict );
			return adobe::value_t( binder );
		}
		return ::dict_function_factory( sheet, name, dict );
	}
	void open_window_eve( const std::string& fileName ) {
		//
		// XXX: We might not want this guy to search up the path.
		//
		bfs::path eve_path( util::getResource( "apps/photo", fileName ) );
		if( !bfs::exists( eve_path ) ) throw std::runtime_error( "open_window: requested filename doesn't exist" );
		controller* c = new controller( _sheet );
		if( c->init() ) c->open_window( eve_path );
		//
		// We let the instance we created look after it's own lifetime.
		// At some point we'll want to put all of these into a list so
		// that we can properly delete open windows when we close.
		//
	}
	void close_window() { delete this; }
	void import_directory_dict( const adobe::dictionary_t& dict ) {
		std::string source_dir( dict.get<std::string>( adobe::static_name_t( "source_dir" ) ) );
		std::string tag( dict.get<std::string>( adobe::static_name_t( "tag" ) ) );
		adobe::binder_ptr_t when_done( dict.get<adobe::binder_ptr_t>( adobe::static_name_t( "when_done" ) ) );
		//
		// Sanity check.
		//
		bfs::path source_path( source_dir, bfs::native );
		if( !bfs::is_directory( source_path ) ) throw std::runtime_error( "import_directory: not a directory" );
		//
		// Do the work.
		//
		toolkit::thread_function_t on_done;
		if( when_done ) on_done = boost::bind( &adobe::binder_t::invoke, when_done );
		_importThread.adoptWork( new import_directory_work_t( source_path, tag, boost::bind( &thread_update_sheet_progress, boost::ref( _sheet ), _1, _2 ), on_done ) );
	}
	void on_camera_add() {
		_sheet.set( adobe::static_name_t( "panel" ), adobe::value_t( std::string( "import" ) ) );
		_sheet.set( adobe::static_name_t( "import_source" ), adobe::value_t( std::string( "camera" ) ) );
		_sheet.update();
	}
  public:
	controller( adobe::sheet_t& sheet ) : _sheet( sheet ), _ws( "", _sheet ) {
		_refCount++;
		_camera_monitor = apps::photo::camera_monitor_t::get_instance();
		_camera_connection = _camera_monitor->on_camera_add( boost::bind( &controller::on_camera_add, this ) );
	}
	~controller() {
		_refCount--;
		if( _refCount == 0 ) toolkit::application::getInstance().quit();
	}
	bool init() {
		//
		// Use our own factory.
		//
		_ws.set_widget_factory(
			boost::bind( &view::factory::build, &_factory, _1, _2, _3, _4 ),
			boost::bind( &view::factory::pop, &_factory ) );
		_ws.set_array_function_factory( boost::bind( &controller::array_function_factory, this, _1, _2, _3 ) );
		_ws.set_dictionary_function_factory( boost::bind( &controller::dictionary_function_factory, this, _1, _2, _3 ) );
		return true;
	}
	//
	// void open_window( const bfs::path& eve )
	// 
	/// Open up a new window from the given Eve file. The new window will
	/// attach to the current sheet.
	///
	/// \param	eve	path to the Eve file to open.
	//
	void open_window( const bfs::path& eve ) {
		bfs::ifstream view_stream( eve );
		_ws.push_back( view_stream,
			adobe::line_position_t::ident_pair_t( adobe::name_t("photoevestream-another"), 0 ),
			size_normal_s );
	}
};
int controller::_refCount = 0;
util::thread controller::_importThread;

//
// For some reason when the filesystem that belongs to a
// Gtk+ file chooser button gets destroyed it releases
// the GDK mutex even though it isn't locked. This was
// causing a deadlock. Using an error checking mutex
// works around this deadlock, as attempting to unlock
// an unlocked error checking mutex fails right away.
//
// The GDK mutex isn't being used for perf. sensitive
// stuff anyway.
//
#ifdef ADOBE_PLATFORM_LINUX
pthread_mutex_t gdk_lock;

void mutex_lock() { pthread_mutex_lock( &gdk_lock ); }
void mutex_unlock() { pthread_mutex_unlock( &gdk_lock ); }
#endif

int main( int argc, char** argv ) {
#ifdef ADOBE_PLATFORM_LINUX
	pthread_mutexattr_t attr;
	pthread_mutexattr_init( &attr );
	pthread_mutexattr_settype( &attr, PTHREAD_MUTEX_ERRORCHECK );
	pthread_mutex_init( &gdk_lock, &attr );
#endif
	//
	// Make sure that we have a database around.
	//
	if( !apps::photo::init_database( getPhotoLibraryPath() ) ) {
		std::cout << "Can't initialize photo database!" << std::endl;
		return 1;
	}
#ifdef ADOBE_PLATFORM_LINUX
	//
	// Need to move this into application -- make application take
	// argc and argv (or something that's cross-platform).
	//
	if( !g_thread_supported() ) {
		g_thread_init(NULL);
		gdk_threads_set_lock_functions( &mutex_lock, &mutex_unlock );
		dbus_g_thread_init();
		gdk_threads_init();
	}
	gtk_init(&argc, &argv);
#endif
	//
	// First initialize the application. This ensures that all extra
	// resources are loaded.
	//
	toolkit::application& the_app = toolkit::application::getInstance();
	if( the_app.init() != toolkit::application::kOk ) return 1;
	try {
		//
		// Create the sheet for the app. We only have one sheet for
		// the whole app.
		//
		adobe::sheet_t sheet;
		bfs::ifstream sheet_stream( util::getResource( "apps/photo", "adam" ) );
		adobe::parse( sheet_stream, adobe::line_position_t(), adobe::bind_to_sheet( sheet ) );
		//
		// Now create and run the controller and app.
		//
		controller* c( new controller( sheet ) );
		if( !c->init() ) return 1;
	        c->open_window( util::getResource( "apps/photo", "eve" ) );
		the_app.run();
	} catch( adobe::stream_error_t& ex ) {
		std::cout << "FATAL EXCEPTION: " << adobe::format_stream_error( ex ) << std::endl;
		adobe::stream_error_t::position_set_t ps( ex.line_position_set() );
		for( unsigned int i = 0; i < ps.size(); i++ ) 
			std::cout << "line: " << ps[i].line_number_m << std::endl;
		return 1;
	} catch( std::exception& ex ) {
		std::cout << "FATAL EXCEPTION: " << ex.what() << std::endl;
		throw;
		return 1;
	} catch( ... ) {
		std::cout << "UNKNOWN FATAL EXCEPTION" << std::endl;
		return 1;
	}
	return 0;
}

//
// This is required for linking against Boost.Thread on Win32.
//
extern "C" void tss_cleanup_implemented() {}
